﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using Djs.Neuronic.Brain;

namespace Djs.Neuronic.Medium
{
    #region interface IRecord : definice vlastností recordu, který je připraven k načítání / vyhledání v databázi, i k ukládání
    /// <summary>
    /// IRecord : definice vlastností recordu, který je připraven k načítání / vyhledání v databázi, i k ukládání.
    /// Objekt tohoto druhu neobsahuje položky, a umí se vyhledávat v paměti i v databázi pouze podle ID (int).
    /// </summary>
    public interface IRecord : IRecordFill, IRecordSave
    { }
    #endregion
    #region interface IRecordFill : definice vlastností recordu, který je možno načítat z databáze
    /// <summary>
    /// IRecordFill : definice vlastností recordu, který je možno načítat z databáze.
    /// Předepisuje pouze metodu Fill(), do níž se posílá Reader obsahující data jednoho řádku, které se mají vepsat do objektu.
    /// </summary>
    public interface IRecordFill
    {
        /// <summary>
        /// Metoda dostává data načtená z databáze (v objektu data) a uloží si je typově do svých property
        /// </summary>
        /// <param name="data"></param>
        void Fill(RecordFillInfo data);
    }
    #endregion
    #region interface IRecordSave : definice vlastností recordu, který je možno ukládat do databáze
    /// <summary>
    /// IRecordSave : definice vlastností recordu, který je možno ukládat do databáze
    /// </summary>
    public interface IRecordSave
    {
        /// <summary>
        /// Identifikátor záznamu. 
        /// Přes interface (IRecordSave) jej lze setovat (při zakládání nového záznamu do databáze dostane svoje ID).
        /// </summary>
        int Id { get; set; }
        /// <summary>
        /// Příznak změn dat v záznamu.
        /// Každá property ve své set metodě má nastavit IsDirty = true;.
        /// Přes interface (IRecord) jej lze setovat (při ukládání záznamu do databáze se nuluje na false).
        /// </summary>
        bool IsDirty { get; set; }
        /// <summary>
        /// Metoda připraví data ze svých property do předaného objektu, odkud budou zapsány do databáze.
        /// Metoda nemá připravovat primární klíče, ukládání jejich hodnoty zajistí jádro.
        /// </summary>
        /// <param name="data"></param>
        void Save(RecordSaveInfo data);
    }
    #endregion
    #region class RecordFillInfo : třída pro podporu načítání dat z readeru do recordu
    /// <summary>
    /// RecordFillInfo : třída pro podporu načítání dat z readeru do recordu
    /// </summary>
    public class RecordFillInfo
    {
        #region Příprava
        internal static RecordFillInfo Prepare(SqlDataReader reader)
        {
            RecordFillInfo info = new RecordFillInfo(reader);
            int cnt = reader.FieldCount;
            for (int c = 0; c < cnt; c++)
            {
                ColumnInfo columnInfo = new ColumnInfo(reader, c);
                if (!info._Columns.ContainsKey(columnInfo.ColumnName))
                    info._Columns.Add(columnInfo.ColumnName, columnInfo);
            }
            return info;
        }
        private RecordFillInfo(SqlDataReader reader)
        {
            this._Columns = new Dictionary<string, ColumnInfo>();
            this._Reader = reader;
        }
        private Dictionary<string, ColumnInfo> _Columns;
        private SqlDataReader _Reader;
        #endregion
        #region Čtení dat
        /// <summary>
        /// Vrátí hodnotu z aktuálního řádku, z daného sloupce.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public object Get(string columnName)
        {
            if (String.IsNullOrEmpty(columnName))
                throw new NeuroSysException("RecordFillInfo.Get(): columnName can not be empty.");
            ColumnInfo columnInfo;
            if (!this.TryGetColumn(columnName, out columnInfo))
                throw new NeuroSysException("RecordFillInfo.Get(): column «" + columnName + "» does not exists in DataReader.");

            return columnInfo.Get();
        }
        /// <summary>
        /// Vrátí hodnotu z aktuálního řádku, z daného sloupce.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public object Get(string columnName, object defaultValue)
        {
            if (String.IsNullOrEmpty(columnName))
                throw new NeuroSysException("RecordFillInfo.Get(): columnName can not be empty.");
            ColumnInfo columnInfo;
            if (!this.TryGetColumn(columnName, out columnInfo))
                return defaultValue;

            return columnInfo.Get(defaultValue);
        }
        #endregion
        #region Hledání sloupce
        /// <summary>
        /// Vrátí true, pokud v datech existuje sloupec daného jména.
        /// Jméno má být zadané lower, trim. Pokud nebude, pak se převede (ale to strojí trochu času).
        /// </summary>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public bool ContainColumn(string columnName)
        {
            ColumnInfo columnInfo;
            return this.TryGetColumn(columnName, out columnInfo);
        }
        /// <summary>
        /// Vrátí true, pokud v datech existuje sloupec daného jména a obsahuje data not null.
        /// Jméno má být zadané lower, trim. Pokud nebude, pak se převede (ale to strojí trochu času).
        /// </summary>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public bool ContainDataNotNull(string columnName)
        {
            ColumnInfo columnInfo;
            if (this.TryGetColumn(columnName, out columnInfo))
                return !columnInfo.IsDBNull;
            return false;
        }
        protected bool TryGetColumn(string columnName, out ColumnInfo columnInfo)
        {
            columnInfo = null;
            if (String.IsNullOrEmpty(columnName)) return false;
            if (!this._Columns.TryGetValue(columnName, out columnInfo))
            {
                if (!this._Columns.TryGetValue(columnName.Trim().ToLower(), out columnInfo))
                    return false;
            }
            return true;
        }
        #endregion
        #region class ColumnInfo
        protected class ColumnInfo
        {
            internal ColumnInfo(SqlDataReader reader, int index)
            {
                this.Index = index;
                this.ColumnName = reader.GetName(index).ToLower().Trim();
                this.DataType = reader.GetFieldType(index);
                this.DataTypeName = this.DataType.Name;
                this.DefaultValue = DatabaseCls.GetDefaultValueOfType(this.DataTypeName);
                this._Reader = reader;
            }
            internal int Index { get; private set; }
            internal string ColumnName { get; private set; }
            internal Type DataType { get; private set; }
            internal string DataTypeName { get; private set; }
            internal object DefaultValue { get; private set; }
            /// <summary>
            /// Reader
            /// </summary>
            private SqlDataReader _Reader;
            /// <summary>
            /// Aktuální hodnota tohoto sloupce
            /// </summary>
            internal object CurrentValue { get { return this._Reader.GetValue(this.Index); } }
            /// <summary>
            /// Aktuální hodnota tohoto sloupce je NULL
            /// </summary>
            internal bool IsDBNull { get { return this._Reader.IsDBNull(this.Index); } }
            public override string ToString()
            {
                return "ColumnName=" + this.ColumnName + "; Index=" + this.Index.ToString() + "; DataType=" + this.DataTypeName + "; Value=" + (this.IsDBNull ? "{NULL}" : this.CurrentValue.ToString());
            }
            internal object Get()
            {
                return this._Get(false, null);
            }
            internal object Get(object defaultValue)
            {
                return this._Get(true, defaultValue);
            }
            private object _Get(bool explicitDefault, object defaultValue)
            {
                object defValue = (explicitDefault ? defaultValue : this.DefaultValue);
                return DatabaseCls.GetReaderValueOfType(this._Reader, this.Index, this.DataTypeName, defValue);
            }
        }
        #endregion
    }
    #endregion
    #region class RecordSaveInfo : třída pro podporu ukládání dat z recordu do databáze + class RecordSavePair
    /// <summary>
    /// RecordSaveInfo : třída pro podporu ukládání dat z recordu do databáze
    /// </summary>
    public class RecordSaveInfo
    {
        #region Konstrukce
        public RecordSaveInfo()
        {
            this.PairList = new List<RecordSavePair>();
        }
        private List<RecordSavePair> PairList;
        #endregion
        #region Vstup dat (sloupec, hodnota)
        /// <summary>
        /// Do záznamníku přidá další položku (sloupec, jeho data)
        /// </summary>
        /// <param name="columnName"></param>
        /// <param name="value"></param>
        public void Add(string columnName, object value)
        {
            this.Add(new RecordSavePair(columnName, value));
        }
        /// <summary>
        /// Do záznamníku přidá další položku (sloupec, jeho data)
        /// </summary>
        /// <param name="pair"></param>
        public void Add(RecordSavePair pair)
        {
            if (this.PairList.Any(p => _EqualColumn(p.ColumnName, pair.ColumnName)))
                throw new NeuroSysException("Column name " + pair.ColumnName + " already exists in RecordSaveInfo.PairList.");
            this.PairList.Add(pair);
        }
        /// <summary>
        /// Ze záznamníku odebere položku pro daný sloupec, pokud tam taková existuje
        /// </summary>
        /// <param name="columnName"></param>
        public void RemoveIfExists(string columnName)
        {
            this.PairList.RemoveAll(p => _EqualColumn(p.ColumnName, columnName));
        }
        /// <summary>
        /// Vyprázdní zásobník
        /// </summary>
        public void Clear()
        {
            this.PairList.Clear();
        }
        private static bool _EqualColumn(string name1, string name2)
        {
            if (name1 == null && name2 == null) return true;
            if (name1 == null || name2 == null) return false;
            return String.Equals(name1.Trim(), name2.Trim(), StringComparison.InvariantCultureIgnoreCase);
        }
        #endregion
        #region Výstup dat (do SQL příkazu INSERT nebo UPDATE)
        /// <summary>
        /// Fráze SQL příkazu INSERT obsahující seznam sloupců (ColumnName) - v závorkách
        /// </summary>
        public string SqlPhraseInsertColumns { get { return "(" + this._SqlPhraseFormatter(pair => pair.ColumnName) + ")"; } }
        /// <summary>
        /// Fráze SQL příkazu INSERT obsahující seznam hodnot (FormattedValue) - v závorkách
        /// </summary>
        public string SqlPhraseInsertValues { get { return "(" + this._SqlPhraseFormatter(pair => pair.FormattedValue) + ")"; } }
        /// <summary>
        /// Fráze SQL příkazu UPDATE obsahující seznam párů ColumnName=FormattedValue - bez závorek
        /// </summary>
        public string SqlPhraseUpdatePairs { get { return this._SqlPhraseFormatter(pair => pair.SqlUpdatePair); } }
        /// <summary>
        /// true, pokud je zásobník prázdný
        /// </summary>
        public bool IsEmpty { get { return (this.PairList.Count == 0); } }
        /// <summary>
        /// Vrátí string vytvořený ze všech položek this.PairList, přičemž text z položky this.PairList[] vytvoří dodaný selector.
        /// </summary>
        /// <param name="selector"></param>
        /// <returns></returns>
        private string _SqlPhraseFormatter(Func<RecordSavePair, string> selector)
        {
            if (this.IsEmpty) return "";
            StringBuilder sb = new StringBuilder();
            foreach (RecordSavePair pair in this.PairList)
                sb.Append(", " + selector(pair));
            return sb.ToString().Substring(2);
        }
        #endregion
    }
    /// <summary>
    /// Pár hodnot pro SQL příkazy ukládání dat (INSERT i UPDATE), pro jeden sloupec (název = hodnota)
    /// </summary>
    public class RecordSavePair
    {
        /// <summary>
        /// Vytvoří pár z dodaných hodnot
        /// </summary>
        /// <param name="columnName"></param>
        /// <param name="value"></param>
        public RecordSavePair(string columnName, object value)
        {
            this.ColumnName = columnName;
            this.FormattedValue = SqlFormat.ToSql(value);
        }
        /// <summary>
        /// Vizualizace
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return this.SqlUpdatePair;
        }
        /// <summary>
        /// Název sloupce
        /// </summary>
        public string ColumnName { get; private set; }
        /// <summary>
        /// Hodnota do sloupce, formátovaná do SQL příkazu
        /// </summary>
        public string FormattedValue { get; private set; }
        /// <summary>
        /// Část SQL příkazu pro UPDATE, ve formě: ColumnName=FormattedValue
        /// </summary>
        public string SqlUpdatePair { get { return this.ColumnName + "=" + this.FormattedValue; } }
    }
    #endregion
    #region interface IMasterRecord + IEntries : definice vlastností recordu, který má nad rámec IRecord i položky
    ///// <summary>
    ///// Master obsahuje sadu seznamů položek.
    ///// </summary>
    //public interface IMasterRecord : IRecord
    //{
    //    /// <summary>
    //    /// Seznam všech objektů seznamů položek
    //    /// </summary>
    //    IEnumerable<IEntries> AllEntries { get; }
    //}
    /// <summary>
    /// Předpis pro jeden seznam položek z hlediska systému
    /// </summary>
    public interface IEntries
    {
        /// <summary>
        /// Datový typ položek v tomto prvku
        /// </summary>
        Type ItemType { get; }
        /// <summary>
        /// Soupis položek, které je třeba zapsat do databáze (jsou nové nebo změněné).
        /// Soupis se čte po uložení prvku Master.
        /// </summary>
        IEnumerable<IRecord> EntriesToSave { get; }
        /// <summary>
        /// Soupis položek, které je třeba vymazat z databáze (jsou načtené, a poté smazané).
        /// Soupis se čte po uložení prvku Master.
        /// </summary>
        IEnumerable<IRecord> EntriesToDelete { get; }
        /// <summary>
        /// Soupis položek, které se právě načetly z databáze.
        /// Třída by je měla uložit do svého soupisu.
        /// Soupis se vkládá po načtení záznamu Master.
        /// </summary>
        IEnumerable<IRecord> EntriesLoaded { set; }
    }
    #endregion
    #region Atributy
    /// <summary>
    /// Atribut definuje komentář.
    /// </summary>
    [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)]
    sealed class RecordCommentAttribute : Attribute
    {
        public RecordCommentAttribute(string comment)
        {
            this._Comment = comment;
        }
        public string Comment { get { return _Comment; } }
        readonly string _Comment;
    }
    /// <summary>
    /// Atribut definuje tabulku v SQL databázi, v níž jsou ukládána data této třídy.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    sealed class RecordTableAttribute : Attribute
    {
        public RecordTableAttribute(string recordTable)
        {
            this._RecordTable = recordTable;
        }
        public string RecordTable { get { return _RecordTable; } }
        readonly string _RecordTable;
    }
    /// <summary>
    /// Atribut definuje SQL SELECT pro načtení dat třídy.
    /// Atribut není povinný, defaultní hodnota je SELECT * FROM (RecordTable).
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    sealed class RecordSelectAttribute : Attribute
    {
        public RecordSelectAttribute(string recordSelect)
        {
            this._RecordSelect = recordSelect;
        }
        public string RecordSelect { get { return _RecordSelect; } }
        readonly string _RecordSelect;
    }

    /// <summary>
    /// Atribut definuje název SQL columnu, který obsahuje ID záznamu.
    /// Atribut není povinný, defaultní hodnota je "id".
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    sealed class RecordColumnIdAttribute : Attribute
    {
        public RecordColumnIdAttribute(string columnId)
        {
            this._ColumnId = columnId;
        }
        public string ColumnId { get { return _ColumnId; } }
        readonly string _ColumnId;
    }
    /// <summary>
    /// Atribut definuje název SQL columnu, který v Entries záznamu obsahuje ID master záznamu.
    /// Podle tohoto sloupce se filtrují záznamy z Entries tabulky při načítání položek k Master záznamu.
    /// Atribut není povinný, defaultní hodnota je "id".
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    sealed class EntriesIdColumnAttribute : Attribute
    {
        public EntriesIdColumnAttribute(string idColumn)
        {
            this._IdColumn = idColumn;
        }
        public string IdColumn { get { return _IdColumn; } }
        readonly string _IdColumn;
    }
    /// <summary>
    /// Atribut definuje režim chování seznamu Entries.
    /// Atribut není povinný, defaultní hodnota je None (položky se nenačítají).
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    sealed class EntriesBehaviorAttribute : Attribute
    {
        public EntriesBehaviorAttribute(EntriesBehaviorMode behaviorMode)
        {
            this._BehaviorMode = behaviorMode;
        }
        public EntriesBehaviorMode BehaviorMode { get { return _BehaviorMode; } }
        readonly EntriesBehaviorMode _BehaviorMode;
    }
    /// <summary>
    /// Režim práce s položkami s pohledu jádra
    /// </summary>
    public enum EntriesBehaviorMode
    {
        /// <summary>
        /// Jádro se o položky nestará
        /// </summary>
        None,
        /// <summary>
        /// Jádro načítá položky po načtení záznamu Master automaticky
        /// </summary>
        Automatic,
        /// <summary>
        /// Jádro položky nenačítá po načtení záznamu Master, 
        /// záznam si v {get} accessoru dokáže vyžádat načtení položek explicitně
        /// (zavolá metodu Database.ReadEntriesTo(this);)
        /// </summary>
        Lazy
    }
    #endregion
}
